/*
 * $Id: LoggerClassGenerator.java 114 2013-01-16 07:33:27Z vladimirow@mail.ru $
 * $HeadURL: http://ultralog.googlecode.com/svn/ultralog/trunk/src/java/com/google/code/ultralog/factory/LoggerClassGenerator.java $
 *
 * This file is a part of ultralog project (http://code.google.com/p/ultralog/).
 * Released under New BSD License.
 */
package com.google.code.ultralog.factory;

import static com.google.code.ultralog.asm.Opcodes.ACC_FINAL;
import static com.google.code.ultralog.asm.Opcodes.ACC_PRIVATE;
import static com.google.code.ultralog.asm.Opcodes.ACC_PUBLIC;
import static com.google.code.ultralog.asm.Opcodes.ALOAD;
import static com.google.code.ultralog.asm.Opcodes.ASTORE;
import static com.google.code.ultralog.asm.Opcodes.ATHROW;
import static com.google.code.ultralog.asm.Opcodes.BIPUSH;
import static com.google.code.ultralog.asm.Opcodes.DLOAD;
import static com.google.code.ultralog.asm.Opcodes.DUP;
import static com.google.code.ultralog.asm.Opcodes.FLOAD;
import static com.google.code.ultralog.asm.Opcodes.GETFIELD;
import static com.google.code.ultralog.asm.Opcodes.GOTO;
import static com.google.code.ultralog.asm.Opcodes.ICONST_0;
import static com.google.code.ultralog.asm.Opcodes.ICONST_1;
import static com.google.code.ultralog.asm.Opcodes.ICONST_2;
import static com.google.code.ultralog.asm.Opcodes.ICONST_3;
import static com.google.code.ultralog.asm.Opcodes.ICONST_4;
import static com.google.code.ultralog.asm.Opcodes.ICONST_5;
import static com.google.code.ultralog.asm.Opcodes.ICONST_M1;
import static com.google.code.ultralog.asm.Opcodes.IFNONNULL;
import static com.google.code.ultralog.asm.Opcodes.IF_ICMPLT;
import static com.google.code.ultralog.asm.Opcodes.ILOAD;
import static com.google.code.ultralog.asm.Opcodes.INVOKEINTERFACE;
import static com.google.code.ultralog.asm.Opcodes.INVOKESPECIAL;
import static com.google.code.ultralog.asm.Opcodes.INVOKEVIRTUAL;
import static com.google.code.ultralog.asm.Opcodes.IRETURN;
import static com.google.code.ultralog.asm.Opcodes.LLOAD;
import static com.google.code.ultralog.asm.Opcodes.NEW;
import static com.google.code.ultralog.asm.Opcodes.PUTFIELD;
import static com.google.code.ultralog.asm.Opcodes.RETURN;
import static com.google.code.ultralog.asm.Opcodes.SIPUSH;
import static com.google.code.ultralog.asm.Opcodes.V1_6;

import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;

import com.google.code.ultralog.asm.ClassWriter;
import com.google.code.ultralog.asm.Label;
import com.google.code.ultralog.asm.MethodVisitor;
import com.google.code.ultralog.asm.Type;
import com.google.code.ultralog.configuration.LoggerConfiguration;
import com.google.code.ultralog.factory.LoggerInterfaceDescriptor.LogLevelCheckerDescriptor;
import com.google.code.ultralog.factory.LoggerInterfaceDescriptor.LogLevelDescriptor;
import com.google.code.ultralog.factory.LoggerInterfaceDescriptor.LogMessageDescriptor;
import com.google.code.ultralog.formatter.BooleanFormatter;
import com.google.code.ultralog.formatter.ByteFormatter;
import com.google.code.ultralog.formatter.CharacterFormatter;
import com.google.code.ultralog.formatter.DoubleFormatter;
import com.google.code.ultralog.formatter.FloatFormatter;
import com.google.code.ultralog.formatter.FormatterFactory;
import com.google.code.ultralog.formatter.IntegerFormatter;
import com.google.code.ultralog.formatter.LongFormatter;
import com.google.code.ultralog.formatter.ReferenceFormatter;
import com.google.code.ultralog.formatter.ShortFormatter;
import com.google.code.ultralog.template.Template;
import com.google.code.ultralog.template.Template.Placeholder;
import com.google.code.ultralog.template.TemplateUtils;
import com.google.code.ultralog.utils.CharacterOutputStream;
import com.google.code.ultralog.writer.LogRecordWriter;
import com.google.code.ultralog.writer.LogRecordWriter.LogRecord;

/**
 * Generates logger classes based on logger interface descriptor.  Generated
 * logger class will be public and non-abstract.  It will have one public
 * constructor with the following signature:
 *
 * <pre>
 * public <i>LoggerClassName</i> (
 *     String loggerName,
 *     LoggerConfiguration loggerConfiguration,
 *     FormatterFactory formatterFactory)</pre>
 *
 * The class will also implement corresponding interface methods for all log
 * level checkers and log messages described in logger interface descriptor.
 *
 * @author Mikhail Vladimirov
 */
public class LoggerClassGenerator
{
    private final LoggerInterfaceDescriptor loggerInterfaceDescriptor;

    private final Map <Integer, String> levelNames =
        new HashMap <Integer, String> ();

    private final Map <FormatterDescriptor, String> formatterNames =
        new LinkedHashMap <LoggerClassGenerator.FormatterDescriptor, String> ();

    /**
     * Create new logger class generator that generates logger classes based on
     * given logger interface descriptor.
     *
     * @param loggerInterfaceDescriptor logger interface descriptor ot use
     */
    public LoggerClassGenerator (
        LoggerInterfaceDescriptor loggerInterfaceDescriptor)
    {
        if (loggerInterfaceDescriptor == null)
            throw new IllegalArgumentException (
                "Logger interface descriptor is null");

        this.loggerInterfaceDescriptor = loggerInterfaceDescriptor;

        int logLevelCount = loggerInterfaceDescriptor.getLogLevelCount ();
        for (int i = 0; i < logLevelCount; i++)
        {
            LogLevelDescriptor logLevelDescriptor =
                loggerInterfaceDescriptor.getLogLevelAt (i);

            levelNames.put (
                Integer.valueOf (logLevelDescriptor.getLevel ()),
                logLevelDescriptor.getName ());
        }
    }

    /**
     * Generate class with given name that implements given interface.
     *
     * @param loggerInterfaceName name of the logger interface to implement
     * @param loggerClassName name of the class to generate
     * @return generated class
     */
    public byte [] generateLoggerClass (
        String loggerInterfaceName,
        String loggerClassName)
    {
        if (loggerInterfaceName == null)
            throw new IllegalArgumentException (
                "Logger interface name is null");

        if (loggerClassName == null)
            throw new IllegalArgumentException (
                "Logger class name is null");

        String loggerInterfaceInternalName =
            loggerInterfaceName.replace ('.', '/');

        String loggerClassInternalName =
            loggerClassName.replace ('.', '/');

        ClassWriter classWriter =
            new ClassWriter (
                ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS);

        classWriter.visit (
            V1_6,
            ACC_PUBLIC,
            loggerClassInternalName,
            null,
            Type.getInternalName (Object.class),
            new String [] {loggerInterfaceInternalName});

        classWriter.visitField (
            ACC_PRIVATE | ACC_FINAL,
            "loggerName",
            Type.getDescriptor (String.class),
            null, null).visitEnd ();

        classWriter.visitField (
            ACC_PRIVATE | ACC_FINAL,
            "loggerConfiguration",
            Type.getDescriptor (LoggerConfiguration.class),
            null, null).visitEnd ();

        int logLevelCheckerCount =
            loggerInterfaceDescriptor.getLogLevelCheckerCount ();
        for (int i = 0; i < logLevelCheckerCount; i++)
        {
            LogLevelCheckerDescriptor logLevelCheckerDescriptor =
                    loggerInterfaceDescriptor.getLogLevelCheckerAt (i);

            generateLevelChecker (
                loggerClassInternalName,
                classWriter.visitMethod (
                    ACC_PUBLIC,
                    logLevelCheckerDescriptor.getMethod ().getName (),
                    Type.getMethodDescriptor (Type.BOOLEAN_TYPE),
                    null,
                    null),
                logLevelCheckerDescriptor.getLevel ());
        }

        int logMessageCount = loggerInterfaceDescriptor.getLogMessageCount ();
        for (int i = 0; i < logMessageCount; i++)
        {
            LogMessageDescriptor logMessageDescriptor =
                    loggerInterfaceDescriptor.getLogMessageAt (i);

            int level = logMessageDescriptor.getLevel ();
            Method method = logMessageDescriptor.getMethod ();

            generateMessage (
                loggerClassInternalName,
                classWriter.visitMethod (
                    ACC_PUBLIC,
                    method.getName (),
                    Type.getMethodDescriptor (method),
                    null,
                    null),
                level,
                getLevelName (level),
                TemplateUtils.parseTemplate (
                    logMessageDescriptor.getTemplate ()),
                method.getParameterTypes ());
        }

        generateConstructor (
            loggerClassInternalName,
            classWriter.visitMethod (
                ACC_PUBLIC,
                "<init>",
                Type.getMethodDescriptor (
                    Type.VOID_TYPE,
                    Type.getType (String.class),
                    Type.getType (LoggerConfiguration.class),
                    Type.getType (FormatterFactory.class)),
                null,
                null));

        for (Map.Entry <FormatterDescriptor, String> entry:
            formatterNames.entrySet ())
        {
            FormatterDescriptor formatterDescriptor = entry.getKey ();
            String formatterName = entry.getValue ();

            Class <?> valueType = formatterDescriptor.valueType;
            String fieldDesc;

            if (Boolean.TYPE.equals (valueType))
                fieldDesc = Type.getDescriptor (BooleanFormatter.class);
            else if (Byte.TYPE.equals (valueType))
                fieldDesc = Type.getDescriptor (ByteFormatter.class);
            else if (Short.TYPE.equals (valueType))
                fieldDesc = Type.getDescriptor (ShortFormatter.class);
            else if (Character.TYPE.equals (valueType))
                fieldDesc = Type.getDescriptor (CharacterFormatter.class);
            else if (Integer.TYPE.equals (valueType))
                fieldDesc = Type.getDescriptor (IntegerFormatter.class);
            else if (Long.TYPE.equals (valueType))
                fieldDesc = Type.getDescriptor (LongFormatter.class);
            else if (Float.TYPE.equals (valueType))
                fieldDesc = Type.getDescriptor (FloatFormatter.class);
            else if (Double.TYPE.equals (valueType))
                fieldDesc = Type.getDescriptor (DoubleFormatter.class);
            else
                fieldDesc = Type.getDescriptor (ReferenceFormatter.class);

            classWriter.visitField (
                ACC_PRIVATE | ACC_FINAL,
                formatterName,
                fieldDesc,
                null,
                null);
        }

        classWriter.visitEnd ();

        return classWriter.toByteArray ();
    }

    private void generateLevelChecker (
        String implName, MethodVisitor methodWriter, int level)
    {
        if (implName == null)
            throw new IllegalArgumentException ("Impl name is null");

        if (methodWriter == null)
            throw new IllegalArgumentException ("Method writer is null");

        Label l0 = new Label ();
        Label l1 = new Label ();
        Label l2 = new Label ();

        // try
        // {
        methodWriter.visitLabel (l0);

        // if (level < loggerConfiguration.getMinimumLoggingLevel ())
        //     return false;
        // else
        //     return true;
        pushInt (methodWriter, level);
        methodWriter.visitVarInsn (ALOAD, 0);
        methodWriter.visitFieldInsn (
            GETFIELD,
            implName,
            "loggerConfiguration",
            Type.getDescriptor (LoggerConfiguration.class));
        methodWriter.visitMethodInsn (
            INVOKEINTERFACE,
            Type.getInternalName (LoggerConfiguration.class),
            "getMinimumLoggingLevel",
            Type.getMethodDescriptor (Type.INT_TYPE));
        methodWriter.visitJumpInsn (IF_ICMPLT, l1);
        methodWriter.visitInsn (ICONST_1);
        methodWriter.visitInsn (IRETURN);
        methodWriter.visitLabel (l1);
        methodWriter.visitInsn (ICONST_0);
        methodWriter.visitInsn (IRETURN);

        // }
        // catch (Throwable ex)
        // {
        //     ex.printStackTeace ();
        //     return false;
        // }
        methodWriter.visitLabel (l2);
        methodWriter.visitVarInsn (ASTORE, 1);
        methodWriter.visitVarInsn (ALOAD, 1);
        methodWriter.visitMethodInsn (
            INVOKEVIRTUAL,
            Type.getInternalName (Throwable.class),
            "printStackTrace",
            Type.getMethodDescriptor (Type.VOID_TYPE));
        methodWriter.visitInsn (ICONST_0);
        methodWriter.visitInsn (IRETURN);

        methodWriter.visitTryCatchBlock (
            l0, l2, l2, Type.getInternalName (Throwable.class));

        methodWriter.visitMaxs (0, 0);
        methodWriter.visitEnd ();
    }

    private void generateMessage (
        String implName, MethodVisitor methodWriter,
        int level, String levelName, Template template,
        Class <?> [] parameterTypes)
    {
        if (implName == null)
            throw new IllegalArgumentException ("Impl name is null");

        if (methodWriter == null)
            throw new IllegalArgumentException ("Method writer is null");

        if (levelName == null)
            throw new IllegalArgumentException ("Level name is null");

        if (template == null)
            throw new IllegalArgumentException ("Template is null");

        if (parameterTypes == null)
            throw new IllegalArgumentException ("Parameter types os null");

        int parameterCount = parameterTypes.length;
        int [] parameterOffsets = new int [parameterCount];
        int offset = 1;
        for (int i = 0; i < parameterCount; i++)
        {
            parameterOffsets [i] = offset;

            Class <?> parameterType = parameterTypes [i];

            if (Long.TYPE.equals (parameterType) ||
                Double.TYPE.equals (parameterType))
                offset += 2;
            else
                offset += 1;
        }

        Label l1 = new Label ();
        Label l2 = new Label ();
        Label l3 = new Label ();
        Label la = new Label ();
        Label l4 = new Label ();
        Label l5 = new Label ();

        // try
        // {
        methodWriter.visitLabel (l1);

        // LoggerConfiguration loggerConfiguration =
        //     this.loggerConfiguration;
        methodWriter.visitVarInsn (ALOAD, 0);
        methodWriter.visitFieldInsn (
            GETFIELD,
            implName,
            "loggerConfiguration",
            Type.getDescriptor (LoggerConfiguration.class));
        methodWriter.visitVarInsn (ASTORE, offset);

        // if (level < loggerConfiguration.getMinumumLoggingLevel ()) return;
        pushInt (methodWriter, level);
        methodWriter.visitVarInsn (ALOAD, offset);
        methodWriter.visitMethodInsn (
            INVOKEINTERFACE,
            Type.getInternalName (LoggerConfiguration.class),
            "getMinimumLoggingLevel",
            Type.getMethodDescriptor (Type.INT_TYPE));
        methodWriter.visitJumpInsn (IF_ICMPLT, l5);

        // LogRecord logRecord =
        //     loggerConfiguration.
        //         getLogRecordWriter ().
        //             beginLogRecord (loggerName, level, levelName);
        methodWriter.visitVarInsn (ALOAD, offset);
        methodWriter.visitMethodInsn (
                INVOKEINTERFACE,
                Type.getInternalName (LoggerConfiguration.class),
                "getLogRecordWriter",
                Type.getMethodDescriptor (
                    Type.getType (LogRecordWriter.class)));
        methodWriter.visitVarInsn (ALOAD, 0);
        methodWriter.visitFieldInsn (
                GETFIELD,
                implName,
                "loggerName",
                Type.getDescriptor (String.class));
        pushInt (methodWriter, level);
        methodWriter.visitLdcInsn (levelName);
        methodWriter.visitMethodInsn (
                INVOKEINTERFACE,
                Type.getInternalName (LogRecordWriter.class),
                "beginLogRecord",
                Type.getMethodDescriptor (
                    Type.getType (LogRecord.class),
                    Type.getType (String.class),
                    Type.INT_TYPE,
                    Type.getType (String.class)));
        methodWriter.visitVarInsn (ASTORE, offset);

        // try
        // {
        methodWriter.visitLabel (l2);

        int placeholderCount = template.getPlaceholderCount ();
        for (int i = 0; i <= placeholderCount; i++)
        {
            String literal = template.getLiteralAt (i);

            if (!literal.isEmpty ())
            {
                // logRecord.write (literal, 0, literal.length ());
                methodWriter.visitVarInsn (ALOAD, offset);
                methodWriter.visitLdcInsn (literal);
                methodWriter.visitInsn (ICONST_0);
                pushInt (methodWriter, literal.length ());
                methodWriter.visitMethodInsn (
                        INVOKEINTERFACE,
                        Type.getInternalName (LogRecord.class),
                        "write",
                        Type.getMethodDescriptor (
                            Type.VOID_TYPE,
                            Type.getType (String.class),
                            Type.INT_TYPE,
                            Type.INT_TYPE));
            }

            if (i < placeholderCount)
            {
                Placeholder placeholder = template.getPlaceholderAt (i);

                String placeholderName = placeholder.getName ();
                int parameterIndex;

                try
                {
                    parameterIndex = Integer.parseInt (placeholderName);
                }
                catch (NumberFormatException ex)
                {
                    throw new IllegalArgumentException (
                        "Placeholder name is not a valid parameter index",
                        ex);
                }

                if (parameterIndex <= 0)
                    throw new IllegalArgumentException (
                        "Parameter index (" + parameterIndex + ") < 0");

                if (parameterIndex > parameterCount)
                    throw new IllegalArgumentException (
                        "Parameter index (" + parameterIndex +
                        ") > parameter count (" + parameterCount + ")");

                parameterIndex -= 1;

                Class <?> parameterType = parameterTypes [parameterIndex];

                Class <?> formatterType;

                if (Boolean.TYPE.equals (parameterType))
                    formatterType = BooleanFormatter.class;
                else if (Byte.TYPE.equals (parameterType))
                    formatterType = ByteFormatter.class;
                else if (Short.TYPE.equals (parameterType))
                    formatterType = ShortFormatter.class;
                else if (Character.TYPE.equals (parameterType))
                    formatterType = CharacterFormatter.class;
                else if (Integer.TYPE.equals (parameterType))
                    formatterType = IntegerFormatter.class;
                else if (Long.TYPE.equals (parameterType))
                    formatterType = LongFormatter.class;
                else if (Float.TYPE.equals (parameterType))
                    formatterType = FloatFormatter.class;
                else if (Double.TYPE.equals (parameterType))
                    formatterType = DoubleFormatter.class;
                else formatterType = ReferenceFormatter.class;

                FormatterDescriptor formatterDescriptor =
                    new FormatterDescriptor (
                        parameterType,
                        placeholder.getOptions ());

                String formatterName = formatterNames.get (formatterDescriptor);
                if (formatterName == null)
                {
                    formatterName = "formatter" + formatterNames.size ();
                    formatterNames.put (formatterDescriptor, formatterName);
                }

                int parameterOffset = parameterOffsets [parameterIndex];

                // formatterXXX.format (argYYY, logRecord);
                methodWriter.visitVarInsn (ALOAD, 0);
                methodWriter.visitFieldInsn (
                    GETFIELD,
                    implName,
                    formatterName,
                    Type.getDescriptor (formatterType));

                if (Boolean.TYPE.equals (parameterType))
                    methodWriter.visitVarInsn (
                        ILOAD, parameterOffset);
                else if (Byte.TYPE.equals (parameterType))
                    methodWriter.visitVarInsn (
                        ILOAD, parameterOffset);
                else if (Short.TYPE.equals (parameterType))
                    methodWriter.visitVarInsn (
                        ILOAD, parameterOffset);
                else if (Character.TYPE.equals (parameterType))
                    methodWriter.visitVarInsn (
                        ILOAD, parameterOffset);
                else if (Integer.TYPE.equals (parameterType))
                    methodWriter.visitVarInsn (
                        ILOAD, parameterOffset);
                else if (Long.TYPE.equals (parameterType))
                    methodWriter.visitVarInsn (
                        LLOAD, parameterOffset);
                else if (Float.TYPE.equals (parameterType))
                    methodWriter.visitVarInsn (
                        FLOAD, parameterOffset);
                else if (Double.TYPE.equals (parameterType))
                    methodWriter.visitVarInsn (
                        DLOAD, parameterOffset);
                else
                    methodWriter.visitVarInsn (
                        ALOAD, parameterOffsets [parameterIndex]);

                methodWriter.visitVarInsn (ALOAD, offset);
                methodWriter.visitMethodInsn (
                        INVOKEINTERFACE,
                        Type.getInternalName (formatterType),
                        "format",
                        Type.getMethodDescriptor (
                            Type.VOID_TYPE,
                            parameterType.isPrimitive () ?
                                Type.getType (parameterType) :
                                Type.getType (Object.class),
                            Type.getType (CharacterOutputStream.class)));
            }
        }

        // }
        // catch (Throwable ex)
        // {
        //     logRecord.cancel ();
        //     throw ex;
        // }
        methodWriter.visitJumpInsn (GOTO, la);
        methodWriter.visitLabel (l3);
        methodWriter.visitVarInsn (ASTORE, offset + 1);
        methodWriter.visitVarInsn (ALOAD, offset);
        methodWriter.visitMethodInsn (
                INVOKEINTERFACE,
                Type.getInternalName (LogRecord.class),
                "cancel",
                Type.getMethodDescriptor (Type.VOID_TYPE));
        methodWriter.visitVarInsn (ALOAD, offset + 1);
        methodWriter.visitInsn (ATHROW);
        methodWriter.visitLabel (la);

        // logRecord.write ();
        methodWriter.visitVarInsn (ALOAD, offset);
        methodWriter.visitMethodInsn (
                INVOKEINTERFACE,
                Type.getInternalName (LogRecord.class),
                "write",
                Type.getMethodDescriptor (Type.VOID_TYPE));

        // }
        // catch (Throwable ex)
        // {
        //     ex.printStackTrace ();
        // }
        methodWriter.visitJumpInsn (GOTO, l5);
        methodWriter.visitLabel (l4);
        methodWriter.visitMethodInsn (
                INVOKEVIRTUAL,
                Type.getInternalName (Throwable.class),
                "printStackTrace",
                Type.getMethodDescriptor (Type.VOID_TYPE));
        methodWriter.visitLabel (l5);

        // return;
        methodWriter.visitInsn (RETURN);

        methodWriter.visitTryCatchBlock (
                l2, l3, l3, Type.getInternalName (Throwable.class));
        methodWriter.visitTryCatchBlock (
                l1, l4, l4, Type.getInternalName (Throwable.class));

        methodWriter.visitMaxs (0, 0);
        methodWriter.visitEnd ();
    }

    private void generateConstructor (
        String implName, MethodVisitor methodWriter)
    {
        if (implName == null)
            throw new IllegalArgumentException ("Impl name is null");

        if (methodWriter == null)
            throw new IllegalArgumentException ("Method writer is null");

        Label l0 = new Label ();
        Label l1 = new Label ();
        Label l2 = new Label ();

        // super ();
        methodWriter.visitVarInsn (ALOAD, 0);
        methodWriter.visitMethodInsn (
            INVOKESPECIAL,
            Type.getInternalName (Object.class),
            "<init>",
            Type.getMethodDescriptor (Type.VOID_TYPE));

        // if (loggerName == null)
        //     throw new IllegalArgumentException ("Logger name is null");
        methodWriter.visitVarInsn (ALOAD, 1);
        methodWriter.visitJumpInsn (IFNONNULL, l0);
        methodWriter.visitTypeInsn (
            NEW, Type.getInternalName (IllegalArgumentException.class));
        methodWriter.visitInsn (DUP);
        methodWriter.visitLdcInsn ("Logger name is null");
        methodWriter.visitMethodInsn (
                INVOKESPECIAL,
                Type.getInternalName (IllegalArgumentException.class),
                "<init>",
                Type.getMethodDescriptor (
                    Type.VOID_TYPE, Type.getType (String.class)));
        methodWriter.visitInsn (ATHROW);
        methodWriter.visitLabel (l0);

        // if (loggerConfiguration == null)
        //     throw new IllegalArgumentException (
        //         "Logger configuration is null");
        methodWriter.visitVarInsn (ALOAD, 2);
        methodWriter.visitJumpInsn (IFNONNULL, l1);
        methodWriter.visitTypeInsn (
                NEW, Type.getInternalName (IllegalArgumentException.class));
        methodWriter.visitInsn (DUP);
        methodWriter.visitLdcInsn ("Logger configuration is null");
        methodWriter.visitMethodInsn (
                INVOKESPECIAL,
                Type.getInternalName (IllegalArgumentException.class),
                "<init>",
                Type.getMethodDescriptor (
                    Type.VOID_TYPE, Type.getType (String.class)));
        methodWriter.visitInsn (ATHROW);
        methodWriter.visitLabel (l1);

        // if (formatterFactory == null)
        //     throw new IllegalArgumentException (
        //         "Formatter factory is null");
        methodWriter.visitVarInsn (ALOAD, 3);
        methodWriter.visitJumpInsn (IFNONNULL, l2);
        methodWriter.visitTypeInsn (
                NEW, Type.getInternalName (IllegalArgumentException.class));
        methodWriter.visitInsn (DUP);
        methodWriter.visitLdcInsn ("Formatter factory is null");
        methodWriter.visitMethodInsn (
                INVOKESPECIAL,
                Type.getInternalName (IllegalArgumentException.class),
                "<init>",
                Type.getMethodDescriptor (
                    Type.VOID_TYPE, Type.getType (String.class)));
        methodWriter.visitInsn (ATHROW);
        methodWriter.visitLabel (l2);

        // this.loggerName = loggerName;
        methodWriter.visitVarInsn (ALOAD, 0);
        methodWriter.visitVarInsn (ALOAD, 1);
        methodWriter.visitFieldInsn (
            PUTFIELD,
            implName,
            "loggerName",
            Type.getDescriptor (String.class));

        // this.loggerConfiguration = loggerConfiguration;
        methodWriter.visitVarInsn (ALOAD, 0);
        methodWriter.visitVarInsn (ALOAD, 2);
        methodWriter.visitFieldInsn (
            PUTFIELD,
            implName,
            "loggerConfiguration",
            Type.getDescriptor (LoggerConfiguration.class));

        for (Map.Entry <FormatterDescriptor, String> entry:
            formatterNames.entrySet ())
        {
            FormatterDescriptor formatterDescriptor = entry.getKey ();
            String formatterName = entry.getValue ();

            Class <?> valueType = formatterDescriptor.valueType;
            String options = formatterDescriptor.options;

            // formatterXXX = formatterFactory.createYYYFormatter (...)
            methodWriter.visitVarInsn (ALOAD, 0);
            methodWriter.visitVarInsn (ALOAD, 3);
            if (!valueType.isPrimitive ())
                methodWriter.visitLdcInsn (Type.getType (valueType));
            methodWriter.visitLdcInsn (options);

            String methodName;
            String methodDesc;
            String fieldDesc;

            if (Boolean.TYPE.equals (valueType))
            {
                methodName = "createBooleanFormatter";
                methodDesc =
                    Type.getMethodDescriptor (
                        Type.getType (BooleanFormatter.class),
                        Type.getType (String.class));
                fieldDesc = Type.getDescriptor (BooleanFormatter.class);
            }
            else if (Byte.TYPE.equals (valueType))
            {
                methodName = "createByteFormatter";
                methodDesc =
                        Type.getMethodDescriptor (
                            Type.getType (ByteFormatter.class),
                            Type.getType (String.class));
                fieldDesc = Type.getDescriptor (ByteFormatter.class);
            }
            else if (Short.TYPE.equals (valueType))
            {
                methodName = "createShortFormatter";
                methodDesc =
                        Type.getMethodDescriptor (
                            Type.getType (ShortFormatter.class),
                            Type.getType (String.class));
                fieldDesc = Type.getDescriptor (ShortFormatter.class);
            }
            else if (Character.TYPE.equals (valueType))
            {
                methodName = "createCharacterFormatter";
                methodDesc =
                        Type.getMethodDescriptor (
                            Type.getType (CharacterFormatter.class),
                            Type.getType (String.class));
                fieldDesc = Type.getDescriptor (CharacterFormatter.class);
            }
            else if (Integer.TYPE.equals (valueType))
            {
                methodName = "createIntegerFormatter";
                methodDesc =
                        Type.getMethodDescriptor (
                            Type.getType (IntegerFormatter.class),
                            Type.getType (String.class));
                fieldDesc = Type.getDescriptor (IntegerFormatter.class);
            }
            else if (Long.TYPE.equals (valueType))
            {
                methodName = "createLongFormatter";
                methodDesc =
                        Type.getMethodDescriptor (
                            Type.getType (LongFormatter.class),
                            Type.getType (String.class));
                fieldDesc = Type.getDescriptor (LongFormatter.class);
            }
            else if (Float.TYPE.equals (valueType))
            {
                methodName = "createFloatFormatter";
                methodDesc =
                        Type.getMethodDescriptor (
                            Type.getType (FloatFormatter.class),
                            Type.getType (String.class));
                fieldDesc = Type.getDescriptor (FloatFormatter.class);
            }
            else if (Double.TYPE.equals (valueType))
            {
                methodName = "createDoubleFormatter";
                methodDesc =
                        Type.getMethodDescriptor (
                            Type.getType (DoubleFormatter.class),
                            Type.getType (String.class));
                fieldDesc = Type.getDescriptor (DoubleFormatter.class);
            }
            else
            {
                methodName = "createReferenceFormatter";
                methodDesc =
                        Type.getMethodDescriptor (
                            Type.getType (ReferenceFormatter.class),
                            Type.getType (Class.class),
                            Type.getType (String.class));
                fieldDesc = Type.getDescriptor (ReferenceFormatter.class);
            }

            methodWriter.visitMethodInsn (
                INVOKEINTERFACE,
                Type.getInternalName (FormatterFactory.class),
                methodName,
                methodDesc);
            methodWriter.visitFieldInsn (
                PUTFIELD,
                implName,
                formatterName,
                fieldDesc);
        }

        // return;
        methodWriter.visitInsn (RETURN);

        methodWriter.visitMaxs (0, 0);
        methodWriter.visitEnd ();
    }

    private static void pushInt (MethodVisitor methodWriter, int value)
    {
        if (value == -1)
            methodWriter.visitInsn (ICONST_M1);
        else if (value == 0)
            methodWriter.visitInsn (ICONST_0);
        else if (value == 1)
            methodWriter.visitInsn (ICONST_1);
        else if (value == 2)
            methodWriter.visitInsn (ICONST_2);
        else if (value == 3)
            methodWriter.visitInsn (ICONST_3);
        else if (value == 4)
            methodWriter.visitInsn (ICONST_4);
        else if (value == 5)
            methodWriter.visitInsn (ICONST_5);
        else if (value >= Byte.MIN_VALUE && value <= Byte.MAX_VALUE)
            methodWriter.visitIntInsn (BIPUSH, value);
        else if (value >= Short.MIN_VALUE && value <= Short.MAX_VALUE)
            methodWriter.visitIntInsn (SIPUSH, value);
        else
            methodWriter.visitLdcInsn (Integer.valueOf (value));
    }

    private String getLevelName (int level)
    {
        String levelName = levelNames.get (Integer.valueOf (level));
        return levelName != null ? levelName : String.valueOf (level);
    }

    private static class FormatterDescriptor
    {
        public final Class <?> valueType;
        public final String options;

        public FormatterDescriptor (Class <?> valueType, String options)
        {
            if (valueType == null)
                throw new IllegalArgumentException ("Value type is null");

            if (options == null)
                throw new IllegalArgumentException ("Options is null");

            this.valueType = valueType;
            this.options = options;
        }

        @Override
        public boolean equals (Object obj)
        {
            if (obj instanceof FormatterDescriptor)
            {
                FormatterDescriptor o = (FormatterDescriptor)obj;

                return
                    valueType.equals (o.valueType) &&
                    options.equals (o.options);

            }
            return false;
        }

        @Override
        public int hashCode ()
        {
            return valueType.hashCode () * 31 + options.hashCode ();
        }
    }
}
